Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add automatic global style injection #48

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

wbern
Copy link

@wbern wbern commented Sep 1, 2020

Fixes #47

Copy link
Member

@marvinhagemeister marvinhagemeister left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making a PR 👍 Made some quick comments, need to do a more thorough review after work 🎉

src/index.js Outdated Show resolved Hide resolved
src/index.js Outdated Show resolved Hide resolved
src/index.test.jsx Outdated Show resolved Hide resolved
src/index.test.jsx Outdated Show resolved Hide resolved
Copy link
Member

@marvinhagemeister marvinhagemeister left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finally had the time to go through the PR in detail. It's amazing to see that you went through all scenarios and even added tests (which is rare for PRs)! Love this 🙌

Left a few comments regarding ways to simplify the API with the intention of keeping maintenance costs low as this library evolves. While tinkering with that I found a few ways to cut down on size.

Let me know what you think 👍


Attaches a Shadow DOM instance to your Custom Element. Remember that global styles won't affect the Custom Element unless you inject (see `options.injectGlobalStyles`) or somehow import them from the component itself.

#### options.injectGlobalStyles
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can slim down the user-facing API surface area. My gut tells me that we can combine the solutions:

  • options.injectGlobalStyles = true is the same as injectGlobalStyles.selector = '*'
  • We could eliminate injectGlobalStyles.target by looping over document.styleSheets directly. It includes both inline style tags and sheets added via a link element. Each sheet has a pointer to the node it was created by and we can match against that.

I'd love to start simple and remove filter too. Whilst there are cases in theory where a query selector may not suffice, I can't come up with realistic real world examples where the selector approach falls short. Checking popular CSS-in-JS libs, they all mark their own stylesheets with a special attribute. For CSS-modules the user usually has tight control over naming the assets and we can match on that.

Since there all sheets are inherently global, unless bound to a shadow root, we can drop the Global part of the variable name, imo.

const defaults = {
target: document.head,
selector:
'style, link[rel="stylesheet"], link[rel="preload"][as="style"]',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strings tend to not compress well. If we switch to document.styleSheets and match our selectors on sheet.ownerNode, we should be able to get rid of it.

observeOptions: { childList: true, subtree: true },
};

this.styleObserver = beginInjectingGlobalStyles(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super tiny nit: The prefix begin* somewhat implies that there is an end* function somewhere. Seeing that this function is only used once we can inline it and save about 14B.

options.injectGlobalStyles === true
? defaults
: /* eslint-disable indent */
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do follow through with the stylesheet approach mentioned earlier we can remove this block.

)
);

return observeStyleChanges(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inlining this function here saves about 23B . It allows us to get rid of the callback function wrapping and additional function call as we can just call cloneElementsToShadowRoot.

) {
return new MutationObserver((mutations, observer) => {
mutations.forEach((mutation) => {
const matchedElements = Array.prototype.slice
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth checking: If we inline cloneElementsToShadowRoot we can loop directly over the NodeList, thereby skipping the additional iteration over elements and saving an array allocation in the process.

return <span>Hello world!</span>;
}

const styleElem = document.createElement('style');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love the tests 🙌

@wbern
Copy link
Author

wbern commented Sep 4, 2020

Wow, good insights! I'll be going through these!

@developit
Copy link
Member

I'm not entirely sure I understand the need for this to be a feature of preact-custom-element. What's happening here is essentially a variant of something the platform has attempted and removed twice (piercing descendant combinator and <style scoped>). From what I recall at the time when those solutions were removed, it turned out to be extremely difficult to achieve desirable semantics + performance when copying whole stylesheets around. Instead, the current approach is to ratify and ship the new Shadow Parts specification.

My concern as it relates to preact-custom-element is that all current userland solutions have poor performance, since even basic optimizations like structural sharing are not possible. For that reason, I would very much like to avoid promoting anything that relies on these semantics. Additionally, manual stylesheet copying would be extremely difficult to support in combination with platform features like Shadow Parts, Declarative Shadow DOM and Template Instantiation.

As a thought: what about providing a list of stylesheets to inject into the shadow root? This would avoid the implicit copying issues, but still allow for selective style inheritance.

@wbern
Copy link
Author

wbern commented Sep 7, 2020

@marvinhagemeister your thoughts on above? Before I put in any more work into the PR.

We could just put a callback right after the attachShadow call, so we in the userland can use a solution like this on our own?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Injecting global styles to shadow: true Custom Elements
3 participants